#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# ======================================================================
#    Copyright (C) 2006 Robert Muth <robert at muth dot org>
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; version 2 of June 1991.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program in form of the file COPYING;
#    if not, write to the
#
#    Free Software Foundation, Inc. <http://www.fsf.org>
#    59 Temple Place, Suite 330,
#    Boston, MA 02111-1307  USA
# ======================================================================

"""
basic data structures for pytrommler

http://muth.org/Robert/pyTrommler

"""
#======================================================================
# python import
import logging


#__all__ = ["Pattern","Drum", "Song"]

# ======================================================================
MAX_DRUM_NUM = 32
MAX_PATT_NUM = 128
MAX_UNITS_NUM = 12
MAX_BEATS_PER_UNITS_NUM = 6
MAX_BEAT_NUM = MAX_UNITS_NUM * MAX_BEATS_PER_UNITS_NUM
MAX_MEASURE_NUM = 256
MAX_UNITS_PER_MINUTE = 200

VERSION_NUM = 4
# ======================================================================
def tokenize(line):
    """
    this lexer is not perfect:
    * no escaping
    * no " inside strings
    * whitespace between strings
    """
    chunks  = line.split('"')
    if len(chunks) % 2 == 0:
        logging.error("bad format")
        return

    tokens = []
    for n, c in enumerate(chunks):
        if n % 2 == 1:
            tokens.append(c)
        else:
            c = c.strip()
            if not c: continue
            tokens += c.split()
    return tokens

# ======================================================================
class Pattern:
    def __init__(self, name):
        self._units = 4
        self._beats_per_unit = 4
        self._name = name
        self._beats = ["." * MAX_BEAT_NUM] * MAX_DRUM_NUM
        return

    def rotate_left(self, n):
        num_beats = self.num_beats()
        if n < 0: n += num_beats
        for i in range(MAX_DRUM_NUM):
            beats = self._beats[i]
            a = beats[:n]
            b = beats[n:num_beats]
            c = beats[num_beats:]
            self._beats[i] = b + a + c
        return

    def name(self): return self._name

    def set_name(self, name):
        self._name = name
        return


    def num_beats(self): return self._units * self._beats_per_unit

    def units(self): return self._units

    def set_units(self, units):
        assert units <= MAX_UNITS_NUM
        self._units = units
        return

    def beats_per_unit(self): return self._beats_per_unit

    def set_beats_per_unit(self, units):
        assert units <= MAX_BEATS_PER_UNITS_NUM
        self._beats_per_unit = units
        return

    def set_beat(self, drum, pos, val):
        assert drum < MAX_DRUM_NUM
        assert pos < MAX_BEAT_NUM
        if val:
            val = "*"
        else:
            val = "."
        beats = self._beats[drum]
        beats = beats[0:pos] + val + beats[pos+1:]
        self._beats[drum] = beats
        return

    def uses_drum(self, drum):
        return "*" in self._beats[drum]

    def beat(self, drum, pos):
        assert drum < MAX_DRUM_NUM
        assert pos < MAX_BEAT_NUM
        return self._beats[drum][pos] == "*"


    def all_beats(self): return self._beats

    def set_all_beats(self, beats):
        assert len(beats) == MAX_DRUM_NUM
        self._beats = beats[:]
        return

    def beats(self, drum):
        assert drum < MAX_DRUM_NUM
        return self._beats[drum]

    def set_beats(self, drum, beats):
        assert drum < MAX_DRUM_NUM
        assert len(beats) == MAX_BEAT_NUM
        self._beats[drum] = beats
        return

# ======================================================================
class Drum:
    def __init__(self,  name):
        self._name = name
        self._path = ""
        self._volume = 1.0
        self._sample = None
        self._panning = 0.0
        self._mute = 0
        self._key = 0
        return

    def name(self): return self._name

    def set_name(self, name):
        self._name = name
        return

    def mute(self): return self._mute

    def set_mute(self, mute):
        self._mute = mute
        return

    def volume(self): return self._volume

    def set_volume(self, volume):
        assert 0.0 <= volume
        self._volume = volume
        return

    def panning(self): return self._panning

    def set_panning(self, panning):
        assert -1.0 <= panning <= 1.0
        self._panning = panning
        return

    def set_key(self, key):
        self._key = key
        return

    def key(self): return self._key

    def set_path(self, path):
        self._path = path
        return

    def path(self): return self._path

    def set_sample(self, sample):
        self._sample = sample
        return

    def sample(self): return self._sample

    def info(self):
        name = self.name()
        path = self.path()
        return '"%s" "%s" %5.1f %5.1f %d %d' % (name,
                                              path,
                                              self.volume(),
                                              self.panning(),
                                              self.mute(),
                                              self.key())
# ======================================================================
class Song:
    def clear(self):
        self._drum = [Drum("drum %02d" % i) for i in range(MAX_DRUM_NUM)]
        self._pattern = [Pattern("pattern %02d" % i) for i in range(MAX_PATT_NUM)]
        self._name = ""
        self._path = ""
        self._length = 0
        self._units_per_minute = 60
        self._measure = [0] * MAX_MEASURE_NUM
        return

    def __init__(self):
        self.clear()
        return

    def name(self): return self._name

    def set_name(self, name):
        self._name = name
        return

    def length(self): return self._length

    def set_length(self, length):
        self._length = length
        return

    def path(self): return self._path

    def set_path(self, path):
        self._path = path
        return

    def units_per_minute(self): return self._units_per_minute

    def set_units_per_minute(self, upms):
        self._units_per_minute = upms
        return

    def pattern(self, i):
        return self._pattern[i]

    def pattern_copy(self, pattern_from, pattern_to):
        pattern_from = self.pattern(pattern_from)
        pattern_to = self.pattern(pattern_to)
        pattern_to.set_beats_per_unit(pattern_from.beats_per_unit())
        pattern_to.set_units(pattern_from.units())
        pattern_to.set_all_beats(pattern_from.all_beats())
        return

    def measure(self, i):
        return self._measure[i]

    def set_measure(self, i, pattern_no):
        self._measure[i] = pattern_no
        return

    def drum(self, i):
        return self._drum[i]

    def drum_swap(self, dr1, dr2):
        drum = self._drum[dr1]
        self._drum[dr1] = self._drum[dr2]
        self._drum[dr2] = drum
        return

    def deserialize(self, s, default_path):
        version = 1
        measure_count = 0
        drum_count = 0
        pattern_count = 0
        beat_count = 0
        song_count = 0
        version_count = 0

        for line in  s.split("\n"):
            line = line.strip()
            if len(line) == 0: continue
            if line.startswith("#"): continue
            tokens = tokenize(line)
            if len(tokens) == 0: continue
            tag = tokens[0]

            if tag == "d":
                drum_count += 1
                if len(tokens) == 8:
                    drum = self.drum(int(tokens[1]))
                    drum.set_name(tokens[2])
                    filename = tokens[3]
                    filename = filename.replace("@", default_path)
                    if version == 3:
                        drum.set_volume(float(tokens[4])/100.0)
                    else:
                        drum.set_volume(float(tokens[4]))
                    drum.set_panning(float(tokens[5]))
                    drum.set_mute(int(tokens[6]))
                    drum.set_key(int(tokens[7]))
                    drum.set_path(filename)
                else:
                    logging.error("bad drum input line " + line)
            elif tag == "v":
                version_count += 1
                if len(tokens) == 2:
                    version = int(tokens[1])
                else:
                    logging.error("bad version input line " + line)

            elif tag == "p":
                pattern_count += 1
                if len(tokens) == 5:
                    pattern = self.pattern(int(tokens[1]))
                    pattern.set_name(tokens[2])
                    pattern.set_units(int(tokens[3]))
                    pattern.set_beats_per_unit(int(tokens[4]))
                else:
                    logging.error("bad pattern input line " + line)

            elif tag == "b":
                beat_count += 1
                if len(tokens) == 4:
                    pattern = self.pattern(int(tokens[1]))
                    drum_no = int(tokens[2])
                    beats = tokens[3]
                    assert len(beats) == pattern.units() * pattern.beats_per_unit()
                    beats += "." * (MAX_BEAT_NUM - len(beats))
                    assert len(beats) == MAX_BEAT_NUM
                    pattern.set_beats(drum_no, beats)
                else:
                    logging.error("bad beat input line " + line)

            elif tag == "s":
                song_count += 1
                if len(tokens) == 4:
                    self.set_name(tokens[1])
                    self.set_units_per_minute(int(tokens[2]))
                    self.set_length(int(tokens[3]))
                else:
                    logging.error("bad input line " + line)
            else:
                try:
                    self.set_measure(measure_count, int(tokens[0]))
                    measure_count += 1
                except:
                    logging.error("bad input line " + line)
        assert version_count == 1
        assert song_count == 1
        assert measure_count == self.length()
        return

    def serialize(self, default_path):
        out = []
        out.append("#")
        out.append("# Songfile created by pyTrommler 1.0 -- http://muth.org/Robert/pyTrommler")
        out.append("#")
        out.append("v %d" % VERSION_NUM)
        out.append("#")
        out.append("# Drums")
        out.append("#")
        for i in xrange(MAX_DRUM_NUM):
            drum = self.drum(i)
            name = drum.name()
            assert '"' not in name
            path = drum.path()
            assert '"' not in path
            assert '@' not in path
            if path.startswith(default_path):
                path = path.replace(default_path, "@", 1)
            out.append('d %2d "%s" "%s" %5.1f %5.1f %d %d' %
                       (i, name, path, drum.volume(), drum.panning(), drum.mute(), drum.key()))
        out.append("#")
        out.append("# Patterns")
        out.append("#")
        for i in xrange(MAX_PATT_NUM):
            pattern = self.pattern(i)
            name = pattern.name()
            assert '"' not in name
            out.append('p %2d "%s" %d %d' %
                       (i, name, pattern.units(), pattern.beats_per_unit()))
            for d in xrange(MAX_DRUM_NUM):
                if pattern.uses_drum(d):
                    beats = pattern.beats(d)[0:pattern.num_beats()]
                    out.append("b %2d %2d %s" % (i, d, beats))
        out.append("#")
        out.append("# Song")
        out.append("#")
        out.append('s "%s" %d %d' % (self.name(), self.units_per_minute(), self.length()))
        for i in xrange(self.length()):
            out.append("%d" % self.measure(i))
        out.append("#")
        out.append("# EOF")
        out.append("#")
        out.append("") # force terminating newline
        return "\n".join(out)


# ======================================================================
